const log = require('j7/log');
const app = require('j7/app');
const bcutils = require('j7/bcutils');
const utils = require('j7/utils');
const http = require('j7/http');
const config = require('j7/config');
const constant = require('common/constant');
const dbpool = require('common/dbpool');
const metaFactory = require('../../../metadata/factory');
const bchelper = require('common/bchelper');
const bcconst = require('common/bcconst');

let gSeqId = 1;
function genSeqId() {
  return ++gSeqId;
}

class BaseEventProcess {

  constructor(proc, conn, eventDb) {
    this.eventProc = proc;
    this.eventDb = eventDb;
    this.returnValues = utils.jsonDecode(this.getEventDb()['return_values']);
    this.blockData = utils.jsonDecode(this.getEventDb()['block_data']);
    this.txHash = this.getEventDb()['txhash'];
    this.bcEventConn = null;
    this.bcNftConn = null;
    this.gameConn = null;
  }

  async safeRelease() {
    try {
      if (this.bcEventConn) {
        this.bcEventConn.release();
        this.bcEventConn = null;
      }
      if (this.bcNftConn) {
        this.bcNftConn.release();
        this.bcNftConn = null;
      }
      if (this.gameConn) {
        this.gameConn.release();
        this.gameConn = null;
      }
    } catch (err) {
      utils.safeDumpErrStack(err);
    }
  }

  genLogHead(msg) {
    return this.eventProc.genLogHead(' activate721nft ' + msg);
  }

  getEventDb() {
    return this.eventDb;
  }

  getBlockNumber() {
    return this.eventDb['block_number'];
  }

  getNetId() {
    return this.eventProc.getNetId();
  }

  getContractAddress() {
    return this.eventProc.getContractAddress();
  }

  getContractName() {
    return this.eventProc.getContractName();
  }

  getContractAddressByName(name) {
    return this.eventProc.getContractAddressByName(name);
  }

  getReturnValues() {
    return this.returnValues;
  }

  getBlockData() {
    return this.blockData;
  }

  getTxHash() {
    return this.txHash;
  }

  throwError(err) {
    const errMsg = this.genLogHead(err);
    throw new Error(errMsg);
  }

  async markOk() {
    await this.updateEventDb(
      [
        ['status', constant.EVENTDB_STATE_HANDLED],
        ['modifytime', utils.getUtcTime()],
      ]
    );
  }

  async updateEventDb(fields) {
    const logHead = this.genLogHead('updateEventDb');
    while (true) {
      const {err} = await this.bcEventDbConn(
        'update',
        this.eventProc.getTableName(),
        [
          ['idx', this.getEventDb()['idx']],
        ],
        fields
      );
      if (!err) {
        break;
      }
      log.error(logHead + err);
      await utils.sleep(5000 + utils.randRange(500, 1500));
    }
  }

  genCbUuid() {
    return utils.compactFormatDate(new Date()) + '_' + app.getPid() + '_' + genSeqId();
  }

  genSecretKey() {
    return utils.genUuid();
  }

  async callGameApi(params, checkCb) {
    const logHead = this.genLogHead('callGameApi');
    while (true) {
      try {
        const nowTime = utils.getUtcTime();
        const cbUuid = this.genCbUuid();
        const secretKey = this.genSecretKey();
        {
          params['_cb_uuid'] = cbUuid;
          params['_timestamp'] = nowTime;
          delete params['_sign'];
        }
        console.log(params);
        const sign = utils.normalMd5UrlSign(params, secretKey);
        params['_sign'] = sign;
        {
          const {err} = await dbpool.execBcNftConn
          (
            app,
            'insert',
            't_callback',
            [
              ['cb_uuid', cbUuid],
              ['params', utils.jsonEncode(params)],
              ['signature', sign],
              ['secret_key', secretKey],
              ['createtime', nowTime],
              ['modifytime', nowTime]
            ]
          );
          if (err) {
            utils.safeDumpErrStack(err);
            log.error(logHead + err);
            await utils.sleep(10000 + utils.randRange(500, 1500));
            continue;
          }
        }
        const {err, data} = await http.get(config('gameapi_url'), params);
        if (err) {
          utils.safeDumpErrStack(err);
          log.error(logHead + err);
          await utils.sleep(5000 + utils.randRange(500, 1500));
          continue;
        }
        console.log(logHead, data);
        if (data['errcode'] == constant.ERRCODE_SIGN_ERROR) {
          log.error(logHead);
          await utils.sleep(5000 + utils.randRange(500, 1500));
          continue;
        }
        const ret = await checkCb(data);
        if (ret) {
          break;
        }
      } catch (e) {
        utils.safeDumpErrStack(e);
        log.error(e);
      }
      await utils.sleep(5000 + utils.randRange(500, 1500));
    }
  }

  async add721NftRefresh(netId, contractAddress, contractName, tokenId) {
    while (true) {
      const nowTime = utils.getUtcTime();
      const {err} = await this.bcEventDbConn(
        'upsert',
        't_erc721_refresh',
        [
          ['net_id', netId],
          ['contract_address', contractAddress],
          ['token_id', tokenId],
        ],
        [
          ['status', 0],
          ['!refresh_count', () => {
            return 'refresh_count + 1';
          }],
          ['modifytime', nowTime]
        ],
        [
          ['net_id', netId],
          ['contract_address', contractAddress],
          ['token_id', tokenId],
          ['status', 0],
          ['refresh_count', 1],
          ['createtime', nowTime],
          ['modifytime', nowTime]
        ]
      );
      if (!err) {
        break;
      }
      await utils.sleep(3000 + utils.randRange(500, 1500));
    }
  }

  async bcEventDbConn(method, ...args) {
    const ret = await this.recreateConn('bcEventConn', constant.BCEVENTDB_NAME);
    if (ret.err) {
      return ret;
    }
    return await this.internalDbConn(ret.conn, method, ...args);
  }

  async bcNftDbConn(method, ...args) {
    const ret = await this.recreateConn('bcNftConn', constant.BCNFTDB_NAME);
    if (ret.err) {
      return ret;
    }
    return await this.internalDbConn(ret.conn, method, ...args);
  }

  async gameDbConn(method, ...args) {
    const ret = await this.recreateConn('gameConn', constant.GAMEDB_NAME);
    if (ret.err) {
      return ret;
    }
    return await this.internalDbConn(ret.conn, method, ...args);
  }

  async recreateConn(connName, dbName) {
    if (!this[connName]) {
      const {err, conn} = await app.getDbConn(dbName);
      if (err) {
        return {
          'err': err,
          'conn': null,
          'row': null,
          'rows': null
        };
      }
      this[connName] = conn;
    }
    return {
      'err': null,
      'conn': this[connName]
    };
  }

  async internalDbConn(conn, method, ...args) {
    const ret = await conn[method](...args);
    return ret;
  }

  async confirmTransactionDb(transId) {
    {
      const nowTime = utils.getUtcTime();
      const {err} = await this.gameDbConn
      (
        'update',
        't_transaction_prefee',
        [
          ['trans_id',  transId],
        ],
        [
          ['done', 1],
          ['modifytime', nowTime],
        ]
      );
      if (err) {
        this.throwError('confirmTransactionDb t_transaction_prefee error transId:' + transId);
      }
    }
    {
      const nowTime = utils.getUtcTime();
      const {err} = await this.gameDbConn
      (
        'update',
        't_transaction',
        [
          ['trans_id',  transId],
        ],
        [
          ['status',  3],
          ['modifytime', nowTime],
        ]
      );
      if (err) {
        this.throwError('confirmTransactionDb t_transaction_prefee error transId:' + transId);
      }
    }
  }

  async exists721Nft(tokenId, contractAddress) {
    const logHead = this.genLogHead(' exists721Nft ');
    const {err, row} = await this.bcNftDbConn(
      'ormSelectOne',
      't_nft',
      [
        ['net_id', this.getNetId()],
        ['contract_address', contractAddress],
        ['token_id', tokenId],
      ]
    );
    if (err) {
      this.throwError(logHead + err);
    }
    return row ? true : false;
  }

  async mint721Nft(address, tokenId, itemId, tokenType,
                   contractAddress, blockNumber) {
    const logHead = this.genLogHead(' mint721Nft ');
    const nowTime = utils.getUtcTime();
    const fieldList = [
      ['token_id', tokenId],
      ['token_type', tokenType],
      ['item_id', itemId],
      ['owner_address', bcutils.toNormalAddress(address)],
      ['creator_address', bcutils.toNormalAddress(address)],
      ['confirm_block_number', blockNumber],
      ['net_id', this.getNetId()],
      ['contract_address', contractAddress],
      ['createtime', nowTime],
      ['modifytime', nowTime],
    ];
    const {err} = await this.bcNftDbConn(
      'upsert',
      't_nft',
      [
        ['token_id', tokenId],
        ['net_id', this.getNetId()],
        ['contract_address', contractAddress],
      ],
      [
      ],
      fieldList
    );
    if (err) {
      this.throwError(logHead + err);
    }
  }

  async update721NftOwner(tokenId, contractAddress, ownerAddress, lastOwnerAddress) {
    const logHead = this.genLogHead(' update721NftOwner ');
    {
      const {err} = await this.bcNftDbConn(
        'update',
        't_nft',
        [
          ['token_id', tokenId],
          ['net_id', this.getNetId()],
          ['contract_address', contractAddress],
        ],
        [
          ['owner_address', bcutils.toNormalAddress(ownerAddress)],
          ['last_owner_address', bcutils.toNormalAddress(lastOwnerAddress)],
        ]
      );
      if (err) {
        this.throwError(logHead + err);
      }
    }
    {
      const nowTime = utils.getUtcTime();
      const from = lastOwnerAddress;
      const to = ownerAddress;
      const lockAddress = this.getContractAddressByName('GoldBrick');
      const srcIdx = this.eventDb['src_idx'];
      if (bcutils.isSysAddress(from) || from == lockAddress ||
         to == lockAddress) {
        const {err} = await this.bcEventDbConn(
          'upsert',
          't_721nft_spec_transfer',
          [
            ['src_idx', srcIdx],
          ],
          [
          ],
          [
            ['net_id', this.eventDb['net_id']],
            ['contract_address', this.eventDb['contract_address']],
            ['token_id', tokenId],
            ['from_address', from],
            ['to_address', to],
            ['src_idx', srcIdx],
            ['createtime', nowTime],
            ['modifytime', nowTime],
          ]
        );
        if (err) {
          this.throwError(logHead + err);
        }
      }
    }
  }

  async mustBeMint(to, tokenId, tokenType) {
    const exists = await this.exists721Nft(tokenId, this.getContractAddress());
    if (!exists) {
      await this.mint721Nft(
        to,
        tokenId,
        0,
        tokenType,
        this.getContractAddress(),
        this.getBlockNumber()
      );
    }
  }

  async ingameActivate(from, to, tokenId, tokenType) {
    const logHead = this.genLogHead(' ingameActivate ');
    const tblName = bchelper.getNftTableName(tokenType);
    if (!tblName) {
      this.throwError(logHead + ' token_type error :' + tokenType);
      return;
    }
    if (tokenType == bcconst.BC_NFT_HERO) {
      await this.ingameActivateHero(from, to, tblName, tokenId, tokenType);
    } else if (tokenType == bcconst.BC_NFT_NORMAL_HERO) {
      await this.ingameActivateHero(from, to, tblName, tokenId, tokenType);
    } else if (tokenType == bcconst.BC_NFT_GOLD_BULLION) {
      await this.ingameActivateGoldBullion(from, to, tblName, tokenId, tokenType);
    }
  }

  async ingameActivateHero(from, to, tblName, tokenId, tokenType) {
    const logHead = this.genLogHead(' ingameActivateHero ');
    const nowTime = utils.getUtcTime();
    {
      const {err, row} = await this.gameDbConn(
        'ormSelectOne',
        tblName,
        [
          ['token_id',  tokenId],
        ]
      );
      if (err) {
        this.throwError(logHead + err);
      }
      if (row) {
        await this.adjustItemId(tokenId, row['hero_id']);
        await this.adjustQuality(tokenId, row['quality']);
        if (row['activate']) {
          return;
        }
      } else {
        await this.addLog([
          ['type', 'ingameActivateHero'],
          ['subtype', 'active_token_id.notfound'],
          ['net_id', this.getNetId()],
          ['param1', tokenId],
          ['createtime', nowTime],
          ['modifytime', nowTime],
        ]);
      }
    }
    const {err} = await this.gameDbConn(
      'update',
      tblName,
      [
        ['token_id',  tokenId],
      ],
      [
        ['activate', 1],
        ['activate_time', nowTime],
        ['modifytime', nowTime],
        ['!account_id',  () => {
          return 'null';
        }],
      ]
    );
    if (err) {
      this.throwError(logHead + err);
    }
  }

  async ingameActivateGoldBullion(from, to, tblName, tokenId, tokenType) {
    const logHead = this.genLogHead(' ingameActivateGoldBullion ');
    const nowTime = utils.getUtcTime();
    const {err, row} = await this.gameDbConn(
      'ormSelectOne',
      tblName,
      [
        ['token_id',  tokenId],
      ]
    );
    if (err) {
      this.throwError(logHead + err);
    }
    if (!row) {
        await this.addLog([
          ['type', 'ingameActivateGoldError'],
          ['subtype', 'token_id.notfound'],
          ['net_id', this.getNetId()],
          ['param1', tokenId],
          ['createtime', nowTime],
          ['modifytime', nowTime],
        ]);
      return;
    }
    if (!row['activated']) {
      const {err} = await this.gameDbConn(
        'update',
        tblName,
        [
          ['token_id',  tokenId],
        ],
        [
          ['activated', 1],
          ['activate_time', nowTime],
        ]
      );
      if (err) {
        this.throwError(logHead + err);
      }
      await this.adjustItemId(tokenId, row['item_id']);
    }
  }

  async adjustItemId(tokenId, itemId) {
    const logHead = ' adjustItemId ';
    if (!tokenId) {
      return;
    }
    if (!itemId) {
      return;
    }
    const {err} = await this.bcNftDbConn(
      'update',
      't_nft',
      [
        ['net_id', this.getNetId()],
        ['contract_address', this.getContractAddress()],
        ['token_id', tokenId],
        ['item_id', 0],
      ],
      [
        ['item_id', itemId],
      ]
    );
    if (err) {
      this.throwError(logHead + err);
    }
  }

  async adjustQuality(tokenId, quality) {
    const logHead = ' adjustQuality ';
    if (!tokenId) {
      return;
    }
    if (!quality) {
      return;
    }
    const {err} = await this.bcNftDbConn(
      'update',
      't_nft',
      [
        ['net_id', this.getNetId()],
        ['contract_address', this.getContractAddress()],
        ['token_id', tokenId],
      ],
      [
        ['quality', quality],
      ]
    );
    if (err) {
      this.throwError(logHead + err);
    }
  }

  async addLog(fieldsKv) {
    const {err} = await this.bcEventDbConn(
      'insert',
      't_log',
      fieldsKv
    );
  }

}

module.exports = BaseEventProcess;
